﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.ServiceModel;
using System.Net;
using System.Xml.Linq;
using System.Data;
using System.Globalization;

namespace ListItemEditor
{
    #region helper types

    public enum FieldType
    {
        Invalid,
        Integer,
        Text,
        Note,
        DateTime,
        Counter,
        Choice,
        Lookup,
        Boolean,
        Number,
        Currency,
        URL,
        Computed,
        Threading,
        Guid,
        MultiChoice,
        GridChoice,
        Calculated,
        File,
        Attachments,
        User,
        Recurrence,
        CrossProjectLink,
        ModStat,
        Error,
        ContentTypeId,
        PageSeparator,
        ThreadIndex,
        WorkflowStatus,
        AllDayEvent,
        WorkflowEventType,
        MaxItems,
        LookupMulti,
        UserMulti
    }

    public class WebData
    {
        public string Url { get; set; }
        public string Title { get; set; }
    }

    public class WebListData
    {
        public string Title { get; set; }
        public string Url { get; set; }
    }

    public class FieldData
    {
        public string Name { get; set; }
        public string DisplayName { get; set; }
        public string Type { get; set; }
        public List<string> FieldRefs { get; set; }
        public FieldType FieldType { get; set; }
        public string LookupList { get; set; }
        public string LookupField { get; set; }
        public string ColName { get; set; }
    }

    public class ViewData
    {
        public Guid ID { get; set; }
        public string Name { get; set; }
        public List<string> ViewFields { get; set; }
        public string Query { get; set; }
        public bool Default { get; set; }
        public string ViewUrl { get; set; }
    }

    public class ListData
    {
        public class ViewData
        {
            public Guid ID { get; set; }
            public string Name { get; set; }
            public bool Default { get; set; }
            public string Url { get; set; }
        }

        public string Name { get; set; }
        public Guid ID { get; set; }
        public bool IsDocLib { get; set; }
        public List<FieldData> Fields { get; set; }
        public List<ViewData> Views { get; set; }
        public ViewData DefaultView { get; set; }
        public string RootFolder { get; set; }
    }

    #endregion

    public class WscHelper
    {
        #region inner types

        private delegate object GetListItemValue(XElement el, FieldData field);

        private class FieldInfo
        {
            public string FieldName { get; set; }
            public Type ColumnType { get; set; }
            public GetListItemValue GetListItemValue { get; set; }
            public FieldData Field { get; set; }
        }

        #endregion

        #region variables
        private static WscHelper _current = new WscHelper();
        private Dictionary<string, ListData> _listDataMap = new Dictionary<string, ListData>();
        private Dictionary<string, ViewData> _viewDataMap = new Dictionary<string, ViewData>();
        private static Dictionary<string, FieldType> _fieldTypeMap = Enum.GetValues(typeof(FieldType)).Cast<FieldType>().ToDictionary(ft => ft.ToString());
        private static Dictionary<Type, GetListItemValue> _getListItemValueMap = new Dictionary<Type, GetListItemValue>()
        {
            { typeof(bool), GetFieldValueBoolean },
            { typeof(int), GetFieldValueInt },
            { typeof(double), GetFieldValueDouble },
            { typeof(DateTime), GetFieldValueDateTime },
            { typeof(string), GetFieldValueString },
        };
        #endregion

        #region static

        private static readonly HashSet<string> _fieldsToRemovePrefix = new HashSet<string>() { "FileLeafRef", "FSObjType" };

        public static BasicHttpSecurityMode SecurityMode { get; set; }
        public static HttpClientCredentialType CredentialType { get; set; }

        static WscHelper() { SecurityMode = BasicHttpSecurityMode.TransportCredentialOnly; CredentialType = HttpClientCredentialType.Ntlm; }

        #endregion

        #region properties
        public static WscHelper Current { get { return _current; } }
        #endregion

        #region public methods

        public List<WebData> GetSubwebs(GridViewDataSourceSettings settings)
        {
            Webs.WebsSoapClient cli = GetWebsClient(settings);
            XElement root = cli.GetWebCollection();

            return root.Elements()
                .Where(el => el.Name.LocalName == "Web")
                .Select(el => new WebData() 
                {
                    Title = el.Attribute("Title").Value,
                    Url = el.Attribute("Url").Value
                }).ToList();
        }

        public void CreateModifyView(GridViewDataSourceSettings settings, ViewData oldViewData, ViewData newViewData)
        {
            Views.ViewsSoapClient viewCl = GetViewsClient(settings);

            XElement viewFieldEl = new XElement("ViewFields", 
                    newViewData.ViewFields.Select(f => new XElement("FieldRef", new XAttribute("Name", f))));

            XElement queryEl = XElement.Parse("<Query>" + (newViewData.Query ?? string.Empty) + "</Query>");
            if (string.Compare(oldViewData.Name, newViewData.Name, false) != 0)
            {
                XElement result = viewCl.AddView(settings.ListName, newViewData.Name, viewFieldEl, queryEl, null, null, newViewData.Default);
            }
            else
            {
                XElement viewProperties = new XElement("View", new XAttribute("DefaultView", newViewData.Default.ToString().ToUpper()));
                XElement result = viewCl.UpdateView(settings.ListName, oldViewData.ID.ToString(), viewProperties, queryEl, viewFieldEl, null, null, null);
            }
            // force refresh
            ListData list = this.GetListData(settings, false);
            ListData.ViewData view = list.Views.FirstOrDefault(v => string.Compare(v.Name, newViewData.Name, false) == 0);
            if (view != null)
            {
                this.InvalidateViewDataCacheForList(settings);
                this.GetViewData(settings, view.ID, false);
            }
        }

        public List<string> GetWebListsTitles(GridViewDataSourceSettings settings)
        {
            Lists.ListsSoapClient listCl = GetListsClient(settings);

            XElement listEl = listCl.GetListCollection();
            return listEl.Elements().Where(el => el.Name.LocalName == "List").Select(el => el.Attribute("Title").Value).ToList();
        }

        public List<WebListData> GetWebLists(GridViewDataSourceSettings settings)
        {
            Lists.ListsSoapClient listCl = GetListsClient(settings);

            XElement listEl = listCl.GetListCollection();
            return listEl.Elements()
                .Where(el => el.Name.LocalName == "List")
                .Select(el => new WebListData() { Title = el.Attribute("Title").Value, Url = this.GetListUrlFromDefaultViewUrl (el.Attribute("DefaultViewUrl").Value) })
                .ToList();
        }

        public ListData GetListData(GridViewDataSourceSettings settings)
        {
            return this.GetListDataInternal(settings, true);
        }

        public ListData GetListData(GridViewDataSourceSettings settings, bool useCache)
        {
            return this.GetListDataInternal(settings, useCache);
        }

        public ViewData GetViewData(GridViewDataSourceSettings settings, Guid viewID)
        {
            return this.GetViewData(settings, viewID, true);
        }

        public DataTable GetListItemData(GridViewDataSourceSettings settings, ListData listData, Guid viewID, bool withRequiredFields)
        {
            return GetListItemData(settings, listData, viewID, withRequiredFields, 0);
        }

        public DataTable GetListItemData(GridViewDataSourceSettings settings, ListData listData, Guid viewID, bool withRequiredFields, uint rowLimit)
        {
            Lists.ListsSoapClient listCl = GetListsClient(settings);

            string viewName = viewID.Equals(Guid.Empty) ? null : viewID.ToString();

            string folderEl = string.IsNullOrEmpty(settings.RootFolder) ? string.Empty : "<Folder>" + Utility.EscapeForXml(settings.RootFolder) +  "</Folder>";
            XElement ndQueryOptions = XElement.Parse("<QueryOptions><IncludeMandatoryColumns>FALSE</IncludeMandatoryColumns><DateInUtc>FALSE</DateInUtc><ViewAttributes Scope='" + settings.ViewScope + "' ModerationType='Moderator' />" + folderEl + "</QueryOptions>");
            
            List<string> viewFields = withRequiredFields ? Utility.GetViewFieldWithRequired(settings.ViewFields, listData.IsDocLib) : settings.ViewFields;
            XElement ndViewFields = XElement.Parse("<ViewFields>" + this.FormatViewFields(viewFields) + "</ViewFields>");
            XElement ndQuery = XElement.Parse("<Query>" + (settings.Query ?? string.Empty) + "</Query>");

            XElement itemsEl = listCl.GetListItems(settings.ListName, viewName, ndQuery, ndViewFields, rowLimit > 0 ? rowLimit.ToString() : "100000", ndQueryOptions, null);

            return this.GetTableFromItemsXml(listData, itemsEl, viewFields);
        }

        public XElement SaveListItemData(GridViewDataSourceSettings settings, string updateXml)
        {
            Lists.ListsSoapClient listCl = GetListsClient(settings);
            return listCl.UpdateListItems(settings.ListName, XElement.Parse(updateXml));
        }

        public DataTable GetTableFromRowElements(ListData listData, List<XElement> elements, List<string> viewFields)
        {
            return this.InternalGetTableFromRowElements(listData, elements, viewFields);
        }

        public bool CheckoutFile(GridViewDataSourceSettings settings, string fileUrl)
        {
            Lists.ListsSoapClient listCl = GetListsClient(settings);
            return listCl.CheckOutFile(fileUrl, "false", null);
        }

        public bool CheckinFile(GridViewDataSourceSettings settings, string fileUrl, CheckOutAction action)
        {
            string type = "MajorCheckin";
            if (action == CheckOutAction.CheckInMinor) type = "MinorCheckin";
            else if (action == CheckOutAction.CheckInOverwrite) type = "OverwriteCheckIn";

            Lists.ListsSoapClient listCl = GetListsClient(settings);
            return listCl.CheckInFile(fileUrl, "", type);
        }

        public bool UndoCheckoutFile(GridViewDataSourceSettings settings, string fileUrl)
        {
            Lists.ListsSoapClient listCl = GetListsClient(settings);
            return listCl.UndoCheckOut(fileUrl);
        }

        #endregion

        #region private

        private ListData GetListDataInternal(GridViewDataSourceSettings settings, bool useCache)
        {
            string webUrl = settings.WebUrl;
            string listName = settings.ListName;

            string key = webUrl.ToLower() + "%%" + listName;
            if (useCache && _listDataMap.ContainsKey(key)) return _listDataMap[key];

            Lists.ListsSoapClient listCl = GetListsClient(settings);

            XElement listEl = listCl.GetList(listName);
            ListData listData = this.GetListDataFromXmlElement(listEl);

            Views.ViewsSoapClient viewCl = GetViewsClient(settings);
            XElement viewEl = viewCl.GetViewCollection(listName);

            this.SetViewsFromXmlElement(listData, viewEl);

            _listDataMap[key] = listData;
            return listData;
        }

        private ViewData GetViewData(GridViewDataSourceSettings settings, Guid viewID, bool useCache)
        {
            string webUrl = settings.WebUrl;
            string listName = settings.ListName;
            string viewName = viewID.ToString();

            string key = GetViewDataCacheKey(webUrl, listName, viewName);
            if (useCache && _viewDataMap.ContainsKey(key)) return _viewDataMap[key];

            XElement root = this.GetViewDataXml(settings, viewID);
            ViewData viewData = new ViewData()
            {
                ID = new Guid(root.Attribute("Name").Value),
                Name = root.Attribute("DisplayName").Value,
                Default = root.Attribute("DefaultView") != null && string.Compare("true", root.Attribute("DefaultView").Value, true) == 0,
                ViewUrl = root.Attribute("Url") == null ? string.Empty : webUrl.TrimEnd('/') + "/" + root.Attribute("Url").Value.TrimStart('/')
            };

            XElement query = root.Elements().Where(el => el.Name.LocalName == "Query").FirstOrDefault();
            if (query != null)
            {
                // strip namespaces
                foreach (XElement el in query.DescendantNodesAndSelf().OfType<XElement>()) el.Name = el.Name.LocalName;
                viewData.Query = InnerXml(query);
            }
            XElement fields = root.Elements().Where(el => el.Name.LocalName == "ViewFields").FirstOrDefault();
            if (fields != null) viewData.ViewFields = fields.Elements().Where(el => el.Name.LocalName == "FieldRef")
                .Select(el => el.Attribute("Name").Value).ToList();

            _viewDataMap[key] = viewData;
            return viewData;
        }

        private void InvalidateViewDataCacheForList(GridViewDataSourceSettings settings)
        {
            ListData list = this.GetListData(settings);
            string webUrl = settings.WebUrl.ToLower();
            foreach (Guid viewID in list.Views.Select(v => v.ID))
            {
                string key = GetViewDataCacheKey(webUrl, settings.ListName, viewID.ToString());
                if (_viewDataMap.ContainsKey(key)) _viewDataMap.Remove(key);
            }
        }

        private string GetViewDataCacheKey(string webUrl, string listName, string viewName)
        {
            string key = webUrl.ToLower() + "%%" + listName + "%%" + viewName;
            return key;
        }

        private XElement GetViewDataXml(GridViewDataSourceSettings settings, Guid viewID)
        {
            Views.ViewsSoapClient viewCl = GetViewsClient(settings);
            XElement viewEl = viewCl.GetView(settings.ListName, viewID.ToString());

            return viewEl;
        }

        private DataTable GetTableFromItemsXml(ListData listData, XElement itemsEl, List<string> viewFields)
        {
            List<XElement> elements;
            XElement dataEl = itemsEl.Elements().Where(el => el.Name.LocalName == "data").FirstOrDefault();
            if (dataEl != null) { elements = dataEl.Elements().Where(el => el.Name.LocalName == "row").ToList(); }
            else elements = new List<XElement>();

            return this.InternalGetTableFromRowElements(listData, elements, viewFields);
        }

        private DataTable InternalGetTableFromRowElements(ListData listData, List<XElement> elements, List<string> viewFields)
        {
            List<FieldInfo> infos = this.GetFieldInfos(listData, viewFields);
            DataTable table = new DataTable();
            table.TableName = listData.Name;

            table.Columns.AddRange(
                    infos.Select(d => new DataColumn(d.FieldName, d.ColumnType)).ToArray());

            foreach (XElement item in elements)
            {
                DataRow row = table.NewRow();
                row.ItemArray = infos.Select(d => d.GetListItemValue(item, d.Field)).ToArray();
                table.Rows.Add(row);
            }

            return table;
        }

        private List<FieldInfo> GetFieldInfos(ListData listData, List<string> viewFields)
        {
            List<FieldInfo> infos = new List<FieldInfo>();
            Dictionary<string, FieldData> fieldMap = listData.Fields.ToDictionary(f => f.Name);

            foreach (string fieldName in viewFields)
            {
                if (!fieldMap.ContainsKey(fieldName)) continue;

                FieldData field = fieldMap[fieldName];
                Type columnType = GetColumnTypeFromFieldType(field);

                GetListItemValue getListItemValue = _fieldsToRemovePrefix.Contains(fieldName) ? GetFieldValueStringRemovePrefix : _getListItemValueMap[columnType];
                FieldInfo info = new FieldInfo() { ColumnType = columnType, Field = field, FieldName = field.Name, GetListItemValue = getListItemValue };
                infos.Add(info);
            }
            return infos;
        }

        private Type GetColumnTypeFromFieldType(FieldData field)
        {
            switch (field.FieldType)
            {
                case FieldType.Integer:
                case FieldType.Counter:
                    return typeof(int);

                case FieldType.Boolean:
                case FieldType.Attachments:
                case FieldType.Recurrence:
                case FieldType.CrossProjectLink:
                    return typeof(bool);

                case FieldType.DateTime:
                    return typeof(DateTime);

                case FieldType.Number:
                case FieldType.Currency:
                    return typeof(double);
                default:
                    return typeof(string);
            }
        }

        private FieldType GetFieldType(string type)
        {
            if (_fieldTypeMap.ContainsKey(type)) return _fieldTypeMap[type];
            return FieldType.Invalid;
        }

        private string FormatViewFields(List<string> viewFields)
        {
            if (viewFields == null) return string.Empty;
            return string.Concat(viewFields.Select(s => string.Format("<FieldRef Name='{0}' Nullable='TRUE' />", s)).ToArray());
        }

        /// <summary>
        /// this was taken from here - http://stackoverflow.com/questions/3793/best-way-to-get-innerxml-of-an-xelement, many thanks to the author
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        private static string InnerXml(XElement element)
        {
            StringBuilder innerXml = new StringBuilder();
            foreach (XNode node in element.Nodes())
            {
                innerXml.Append(node.ToString());
            }
            return innerXml.ToString();
        }

        private void SetViewsFromXmlElement(ListData listData, XElement root)
        {
            listData.Views = root.Elements().Where(el => el.Name.LocalName == "View").
                Select(el => new ListData.ViewData()
                {
                    ID = new Guid (el.Attribute("Name").Value),
                    Name = el.Attribute("DisplayName").Value,
                    Url = el.Attribute("Url").Value,
                    Default = string.Compare("true", el.Attribute("DefaultView") == null ? "" : el.Attribute("DefaultView").Value, true) == 0
                }).ToList();
            listData.DefaultView = listData.Views.Where(v => v.Default).FirstOrDefault();
        }

        private ListData GetListDataFromXmlElement(XElement root)
        {
            return new ListData() 
            { 
                ID = new Guid(root.Attribute("ID").Value),
                IsDocLib = "1".Equals(root.Attribute("BaseType").Value),
                Name = root.Attribute("Title").Value, 
                RootFolder = root.Attribute("RootFolder").Value,
                Fields =
                    root.Elements().Where(e => e.Name.LocalName == "Fields").First().Elements().Where(e => e.Name.LocalName == "Field").
                        Select(el => new FieldData() 
                        { 
                            Name = el.Attribute("Name").Value,
                            DisplayName = el.Attribute("DisplayName").Value, 
                            Type = el.Attribute("Type").Value,
                            FieldType = GetFieldType(el.Attribute("Type").Value),
                            LookupField = el.Attribute("ShowField") == null ? null : el.Attribute("ShowField").Value,
                            ColName = el.Attribute("ColName") == null ? null : el.Attribute("ColName").Value,
                            LookupList = el.Attribute("List") == null ? null : el.Attribute("List").Value,
                            FieldRefs = el.Descendants().Where(e => e.Name.LocalName == "FieldRef" && e.Parent.Name.LocalName == "FieldRefs").Select(e => e.Attribute("Name").Value).ToList()
                        }).
                        ToList()
            };
        }

        private Lists.ListsSoapClient GetListsClient(GridViewDataSourceSettings settings)
        {
            Uri webUri = new Uri(settings.WebUrl);
            string url = webUri.AbsoluteUri.TrimEnd('/') + "/_vti_bin/Lists.asmx";
            EndpointAddress addr = new EndpointAddress(url);
            Lists.ListsSoapClient cl = new Lists.ListsSoapClient(GetBindingForSharePoint(), addr);
            cl.ClientCredentials.Windows.ClientCredential = !string.IsNullOrEmpty(settings.UserName) ? new NetworkCredential(settings.UserName, settings.Password) : new NetworkCredential();
            cl.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
            return cl;
        }

        private Views.ViewsSoapClient GetViewsClient(GridViewDataSourceSettings settings)
        {
            Uri webUri = new Uri(settings.WebUrl);

            string url = webUri.AbsoluteUri.TrimEnd('/') + "/_vti_bin/Views.asmx";
            EndpointAddress addr = new EndpointAddress(url);
            Views.ViewsSoapClient cl = new Views.ViewsSoapClient(GetBindingForSharePoint(), addr);
            cl.ClientCredentials.Windows.ClientCredential = string.IsNullOrEmpty (settings.UserName) ? new NetworkCredential(settings.UserName, settings.Password) : new NetworkCredential();
            cl.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
            return cl;
        }

        private Webs.WebsSoapClient GetWebsClient(GridViewDataSourceSettings settings)
        {
            Uri webUri = new Uri(settings.WebUrl);

            string url = webUri.AbsoluteUri.TrimEnd('/') + "/_vti_bin/Webs.asmx";
            EndpointAddress addr = new EndpointAddress(url);
            Webs.WebsSoapClient cl = new Webs.WebsSoapClient(GetBindingForSharePoint(), addr);
            cl.ClientCredentials.Windows.ClientCredential = string.IsNullOrEmpty(settings.UserName) ? new NetworkCredential(settings.UserName, settings.Password) : new NetworkCredential();
            cl.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
            return cl;
        }

        private BasicHttpBinding GetBindingForSharePoint()
        {
            BasicHttpBinding binding = new BasicHttpBinding(WscHelper.SecurityMode);
            binding.Security.Transport.ClientCredentialType = WscHelper.CredentialType;
            binding.ReaderQuotas.MaxStringContentLength = 999999999;
            binding.ReaderQuotas.MaxNameTableCharCount = 999999999;
            binding.ReaderQuotas.MaxDepth = 999999999;
            binding.ReaderQuotas.MaxBytesPerRead = 999999999;
            binding.ReaderQuotas.MaxArrayLength = 999999999;
            binding.MaxReceivedMessageSize = 999999999;
            return binding;
        }

        private static object GetFieldValueString(XElement el, FieldData field)
        {
            XAttribute attr = el.Attribute("ows_" + field.Name);
            if (attr == null) return DBNull.Value;
            return attr.Value;
        }

        private static object GetFieldValueStringRemovePrefix(XElement el, FieldData field)
        {
            object value = GetFieldValueString(el, field);
            return DBNull.Value.Equals(value) ? (object)value : Utility.ParseLookupValue (value.ToString());
        }

        private static object GetFieldValueDouble(XElement el, FieldData field)
        {
            object value = GetFieldValueString(el, field);
            return DBNull.Value.Equals(value) ? (object)value : (object)Convert.ToDouble(value, CultureInfo.InvariantCulture);
        }

        private static object GetFieldValueInt(XElement el, FieldData field)
        {
            object value = GetFieldValueString(el, field);
            return DBNull.Value.Equals(value) ? (object)value : (object)Convert.ToInt32(value, CultureInfo.InvariantCulture);
        }

        private static object GetFieldValueBoolean(XElement el, FieldData field)
        {
            object value = GetFieldValueString(el, field);
            if (DBNull.Value.Equals(value)) return value;

            string str = value.ToString();
            if (string.IsNullOrEmpty(str)) return DBNull.Value;
            return str.Equals("1") || string.Compare(str, "true", true) == 0;
        }

        private static object GetFieldValueDateTime(XElement el, FieldData field)
        {
            object value = GetFieldValueString(el, field);
            return DBNull.Value.Equals(value) ? (object)value : (object)CreateSystemDateTimeFromXmlDataDateTimeFormat(value.ToString());
        }

        private static DateTime CreateSystemDateTimeFromXmlDataDateTimeFormat(string strDT)
        {
            return new DateTime(Convert.ToInt32(strDT.Substring(0, 4)), Convert.ToInt32(strDT.Substring(5, 2)), Convert.ToInt32(strDT.Substring(8, 2)), Convert.ToInt32(strDT.Substring(11, 2)), Convert.ToInt32(strDT.Substring(14, 2)), Convert.ToInt32(strDT.Substring(0x11, 2)), new GregorianCalendar());
        }

        private XmlElement GetXmlElementFromString(XmlDocument doc, string xml)
        {
            XmlElement outer = doc.CreateElement("z");
            outer.InnerXml = xml;
            return (XmlElement)outer.FirstChild;
        }

        private string GetListUrlFromDefaultViewUrl(string url)
        {
            string[] parts = url.Split('/');
            if (parts.Length > 1 && string.Compare (parts[parts.Length - 2], "forms", true) == 0) return string.Join ("/", parts, 0, parts.Length - 2);
            else if (parts.Length > 0) return string.Join("/", parts, 0, parts.Length - 1);
            return url;
        }

        #endregion
    }
}